const vscode = require('vscode');
const path = require('path');
const fs = require('fs');
const prototype = require('./prototype');
const { modify, applyEdits } = require('jsonc-parser');

const JSON_OPTIONS = {
  formattingOptions: {
    insertSpaces: true,
    tabSize: 2,
    eol: '\n',
  },
};

class PlatoCategoryTreeProvider {
  constructor(context) {
    context.subscriptions.push(vscode.workspace.onDidChangeWorkspaceFolders(() => this.refresh()));
    context.subscriptions.push(vscode.commands.registerCommand('plato.newCategory', () => this.newCategory()));
    context.subscriptions.push(vscode.commands.registerCommand('plato.deleteCategory', (x) => this.deleteCategory(x)));
    context.subscriptions.push(vscode.commands.registerCommand('plato.renameCategory', (x) => this.renameCategory(x)));
    context.subscriptions.push(vscode.commands.registerCommand('plato.newNode', (x) => this.newNode(x)));
    context.subscriptions.push(vscode.commands.registerCommand('plato.deleteNode', (x) => this.deleteNode(x)));
    context.subscriptions.push(vscode.commands.registerCommand('plato.renameNode', (x) => this.renameNode(x)));
    context.subscriptions.push(vscode.commands.registerCommand('plato.markNodeServer', (x) => this.markNode(x, true)));
    context.subscriptions.push(vscode.commands.registerCommand('plato.markNodeClient', (x) => this.markNode(x, false)));
    context.subscriptions.push(vscode.commands.registerCommand('plato.newPin', (x) => this.newPin(x)));
    context.subscriptions.push(vscode.commands.registerCommand('plato.deletePin', (x) => this.deletePin(x)));
    context.subscriptions.push(vscode.commands.registerCommand('plato.renamePin', (x) => this.renamePin(x)));

    this._onDidChangeTreeData = new vscode.EventEmitter();
    this.onDidChangeTreeData = this._onDidChangeTreeData.event;
    this.refresh();
  }

  refresh() {
    if (this.watcher) {
      this.watcher.dispose();
    }

    if (!vscode.workspace.workspaceFolders) {
      vscode.commands.executeCommand('setContext', 'plato:categoryInitialized', false);
      return;
    }

    const root = vscode.workspace.workspaceFolders[0].uri.fsPath;
    this.path = path.join(root, 'plato.json');

    this.watcher = vscode.workspace.createFileSystemWatcher('**/*.json', false, false, false);
    this.watcher.onDidChange((e) => {
      if (e.fsPath === this.path) {
        this.refresh();
      }
    });
    this.watcher.onDidCreate((e) => e.fsPath === this.path && this.refresh());
    this.watcher.onDidDelete((e) => e.fsPath === this.path && this.refresh());

    if (!fs.existsSync(this.path)) {
      vscode.commands.executeCommand('setContext', 'plato:categoryInitialized', false);
      return;
    }

    try {
      this.text = fs.readFileSync(this.path, 'utf-8');
      const json = JSON.parse(this.text);
      this.categories = json.categories;
      this.structs = json.structs;
      this.maxId = 0;
      for (const category of this.categories) {
        for (const node of category.nodes) {
          this.maxId = Math.max(this.maxId, node.id);
        }
      }

      vscode.commands.executeCommand('setContext', 'plato:categoryInitialized', true);
      this._onDidChangeTreeData.fire();
    } catch (err) {
      vscode.window.showErrorMessage(err);
      vscode.commands.executeCommand('setContext', 'plato:categoryInitialized', false);
      return;
    }
  }

  getTreeItem(element) {
    return element;
  }

  getChildren(element) {
    if (element) {
      if (element instanceof CategoryItem) {
        return Promise.resolve(element.target.nodes.map((x) => new NodeItem(x, element)));
      } else if (element instanceof NodeItem) {
        return Promise.resolve([
          new PinsItem('inputs', element.target.inputs, element),
          new PinsItem('outputs', element.target.outputs, element),
        ]);
      } else if (element instanceof PinsItem) {
        return Promise.resolve(element.target.map((x) => new PinItem(x, element)));
      } else {
        return Promise.resolve([]);
      }
    } else {
      return Promise.resolve(this.categories.map((x) => new CategoryItem(x)));
    }
  }

  updateDocument(path, value) {
    const edits = modify(this.text, path, value, JSON_OPTIONS);
    fs.writeFileSync(this.path, applyEdits(this.text, edits));
  }

  newCategory() {
    let title;
    let color;

    vscode.window
      .showInputBox({ prompt: 'New Category:', placeHolder: 'Title' })
      .then((val) => {
        if (val) {
          title = val;
          return vscode.window.showQuickPick(prototype.COLORS, { placeHolder: 'Color', canPickMany: false });
        }
      })
      .then((val) => {
        if (val) {
          color = val;
          this.updateDocument(['categories', this.categories.length], {
            title,
            color,
            nodes: [],
          });
        }
      });
  }

  deleteCategory(item) {
    const index = this.categories.indexOf(item.target);
    if (index >= 0) {
      this.updateDocument(['categories', index], undefined);
    }
  }

  renameCategory(item) {
    const index = this.categories.indexOf(item.target);
    vscode.window.showInputBox({ prompt: 'Rename Category', value: item.target.title }).then((val) => {
      if (val) {
        this.updateDocument(['categories', index], {
          ...item.target,
          title: val,
        });
      }
    });
  }

  newNode(item) {
    const index = this.categories.indexOf(item.target);
    const nodes = this.categories[index].nodes;
    vscode.window.showInputBox({ prompt: 'New Node:', placeHolder: 'Title' }).then((val) => {
      if (val) {
        this.updateDocument(['categories', index, 'nodes', nodes.length], {
          id: ++this.maxId,
          title: val,
          inputs: [],
          outputs: [],
        });
      }
    });
  }

  deleteNode(item) {
    const categoryIndex = this.categories.indexOf(item.parent.target);
    const nodeIndex = this.categories[categoryIndex].nodes.indexOf(item.target);
    this.updateDocument(['categories', categoryIndex, 'nodes', nodeIndex], undefined);
  }

  renameNode(item) {
    const categoryIndex = this.categories.indexOf(item.parent.target);
    const nodeIndex = this.categories[categoryIndex].nodes.indexOf(item.target);
    vscode.window.showInputBox({ prompt: 'Rename Node:', value: item.target.title }).then((val) => {
      if (val) {
        this.updateDocument(['categories', categoryIndex, 'nodes', nodeIndex], {
          ...item.target,
          title: val,
        });
      }
    });
  }

  markNode(item, value) {
    const categoryIndex = this.categories.indexOf(item.parent.target);
    const nodeIndex = this.categories[categoryIndex].nodes.indexOf(item.target);
    this.updateDocument(['categories', categoryIndex, 'nodes', nodeIndex, 'isServer'], value);
  }

  newPin(item) {
    let title;
    let type;
    let key;
    let value;
    let pins;

    const nodeItem = item.parent;
    const categoryItem = nodeItem.parent;
    const categoryIndex = this.categories.indexOf(categoryItem.target);
    const nodeIndex = this.categories[categoryIndex].nodes.indexOf(nodeItem.target);
    const isInput = item.contextValue === 'inputs';
    if (isInput) {
      pins = nodeItem.target.inputs;
    } else {
      pins = nodeItem.target.outputs;
    }

    vscode.window
      .showInputBox({ placeHolder: 'Title', prompt: 'New Pin: ' })
      .then((val) => {
        if (val) {
          title = val;
          return vscode.window.showQuickPick(prototype.ALL_TYPES.concat(this.structs.map((x) => x.title)), {
            placeHolder: 'Type',
            canPickMany: false,
          });
        }
      })
      .then((val) => {
        if (val) {
          type = val;
          if (prototype.hasKey(type)) {
            return vscode.window.showQuickPick(prototype.SIMPLE_TYPES.concat(this.structs.map((x) => x.title)), {
              placeHolder: 'Key',
              canPickMany: false,
            });
          } else {
            this.updateDocument(['categories', categoryIndex, 'nodes', nodeIndex, item.contextValue, pins.length], {
              title,
              type,
              key,
              value,
            });
          }
        }
      })
      .then((val) => {
        if (val) {
          key = val;
          if (prototype.hasValue(type)) {
            return vscode.window.showQuickPick(prototype.SIMPLE_TYPES.concat(this.structs.map((x) => x.title)), {
              placeHolder: 'Value',
              canPickMany: false,
            });
          } else {
            this.updateDocument(['categories', categoryIndex, 'nodes', nodeIndex, item.contextValue, pins.length], {
              title,
              type,
              key,
              value,
            });
          }
        }
      })
      .then((val) => {
        if (val) {
          value = val;
          this.updateDocument(['categories', categoryIndex, 'nodes', nodeIndex, item.contextValue, pins.length], {
            title,
            type,
            key,
            value,
          });
        }
      });
  }

  getPinPath(item) {
    const pinsItem = item.parent;
    const nodeItem = pinsItem.parent;
    const categoryItem = nodeItem.parent;
    const categoryIndex = this.categories.indexOf(categoryItem.target);
    const nodeIndex = this.categories[categoryIndex].nodes.indexOf(nodeItem.target);
    let pinIndex;
    if (pinsItem.contextValue === 'inputs') {
      pinIndex = nodeItem.target.inputs.indexOf(item.target);
    } else {
      pinIndex = nodeItem.target.outputs.indexOf(item.target);
    }
    return ['categories', categoryIndex, 'nodes', nodeIndex, pinsItem.contextValue, pinIndex];
  }

  deletePin(item) {
    const path = this.getPinPath(item);
    this.updateDocument(path, undefined);
  }

  renamePin(item) {
    const path = this.getPinPath(item);
    vscode.window.showInputBox({ prompt: 'Rename Pin:', value: item.target.title }).then((val) => {
      if (val) {
        this.updateDocument(path, {
          ...item.target,
          title: val,
        });
      }
    });
  }
}

class PinItem extends vscode.TreeItem {
  constructor(target, parent) {
    super(target.title, vscode.TreeItemCollapsibleState.None);
    this.target = target;
    this.parent = parent;
    this.tooltip = prototype.typeLabel(target.type, target.key, target.value);
    this.iconPath = new vscode.ThemeIcon(prototype.typeIcon(target.type));
    this.contextValue = 'pin';
  }
}

class PinsItem extends vscode.TreeItem {
  constructor(label, target, parent) {
    super(label, vscode.TreeItemCollapsibleState.Collapsed);
    this.target = target;
    this.parent = parent;
    this.contextValue = label;
    this.iconPath = new vscode.ThemeIcon('symbol-null');
  }
}

class NodeItem extends vscode.TreeItem {
  constructor(target, parent) {
    super(target.title, vscode.TreeItemCollapsibleState.Collapsed);
    this.target = target;
    this.parent = parent;
    this.iconPath = new vscode.ThemeIcon('symbol-method');
    this.contextValue = 'node';
  }
}

class CategoryItem extends vscode.TreeItem {
  constructor(target) {
    super(target.title, vscode.TreeItemCollapsibleState.Collapsed);
    this.target = target;
    this.iconPath = new vscode.ThemeIcon('symbol-class', new vscode.ThemeColor(`plato.${target.color}`));
    this.contextValue = 'category';
  }
}

module.exports = PlatoCategoryTreeProvider;
